#include <stdio.h>          //perror() , sprintf()-将格式化数据写入字符串
#include <stdlib.h>
#include <string.h>
#include <errno.h>          //strerror()
#include <sys/types.h>      //opendir(),readdir() ,struct stat，getgrgid()，getpwuid(),geteuid()
#include <dirent.h>         //opendir(), readdir()
#include <sys/stat.h>       //struct stat , stat()
#include <unistd.h>         //stat() ,access(),geteuid()
#include <limits.h>         //PATH_MAX  最长的路径名
#include <grp.h>            //getgrgid()
#include <pwd.h>            //getpwuid()
#include <time.h>           //ctime()
#include <fcntl.h>          //access()

//如果DEBUG为true,则输出函数名称和行号,然后输出您想要打印的内容
//注意  这个报错宏并不会退出进程，你可以使用exit(-1) 来手动退出进程

#define PARAMETER_NONE 0
//下面是学习计网课程时给我带来的思路

/*将得到的用户选项转化成一个整型变量user_option，用于存储可能的选项
像下面设计用于检测用户选项的宏，与运算得到1说明含有此参数
比如user_option=12,则和PARAMETER_D、PARAMETER_I 位与运算 结果为1，
我们便可以知道用户输入的选项是 -i -d*/


#define PARAMETER_A 1   //显示隐藏文件 包括 .开头的隐藏文件以及代表当前目录的.和父目录的..
#define PARAMETER_F 2   //文件名后加字符显示文件类型
#define PARAMETER_I 4   //显示i-node number
#define PARAMETER_D 8   //对于目录参数，不显示子目录，只显示目录文件
#define PARAMETER_L 16  //长格式显示文件
#define PARAMETER_R 32  //递归显示子目录
#define MAXROWLEN 80    //一行显示的最长字符数
int DEBUG=1;
#define DBGLOG(format, ...) if(DEBUG){ fprintf(stderr, "%s -- %d -- ", __FUNCTION__, __LINE__); fprintf(stderr, format, ##__VA_ARGS__);}






int g_left_len=MAXROWLEN  ;  //一行剩余长度，用于输出补齐
int g_maxlen=0;             //用于存放当前处理的目录下的最长的文件名
int g_user_option=0;      //用来存储用户选项
int g_recursion=0;          //maybe need recursion 可能需要递归处理，当文件实参是目录时，如果它含有子目录且用户指定了-R选项且没有指定-d选项，则递归处理下一层子目录的内容
char r_dir_arr[50][PATH_MAX];   //需要递归处理的子目录名字
int r_dir_number=0;     //需要递归处理的子目录个数
int r_dir_index=0;      //当前处理到的子目录在r_dir_arr中的下标
int is_in_child_dir=0;      //当前工作路径是否在子目录中
char path[PATH_MAX+1];      //当前处理的路径
char initial_path[PATH_MAX+1];     //用户运行程序时的所在路径 即 .

void test_option();                 //用于检测程序对 用户输入的选项 的识别结果
void do_ls(char* dirname);          //访问各个目录
void do_stat(char* filename);       //获取各个文件信息
void show_file_info(char * filename,struct stat *file_info);//展示单个文件的信息
void mode_to_str(struct stat *file_info, char * str,char * filename);      //将文件mode转换成一个长度为11的C风格字符串（第10位存的是'\0'）
char * uid_to_name(uid_t uid);
char * gid_to_name(gid_t gid);
char file_type_char(mode_t    st_mode,uid_t uid);         //得到文件对应的类型字符






int main(int argc,char **argv)
{

    int i,k;//循环变量
    int file_index=1;   //第一个非选项的文件实参的下标
    char buf[80];
    getcwd(buf,sizeof(buf));
    strcpy(initial_path,buf);           //存储下用户的当前目录 . (用pwd得到的那个路径)

    printf("\n");

    /*
        下面我采用双层循环对命令行参数进行解析，即提取命令行参数中的ls的选项信息，
        采用双层循环的原因是考虑到用户输入选项的方式可能是-l -d 也可能是-ld，
        所以采用外层循环提取到所有的'-'开头的选项,
        (这里注意，shell中使用“--” 作为选项的结束，所以读到“--”以后，之后哪怕读到"-l",也不能当做选项来解读)，
        内层循环对'-'之后的字符串进行分析，同时对user_option进行赋值，以便之后进行用户选项的处理

    */

    if(argc==1){        //无选项，无文件名实参
        //test_option();
        do_ls(".");

    }else {

    int argc_n=argc;        //暂存argc的值，用于for循环
    for(i=1;i<argc_n;i++){

        if(argv[i][0]=='-'){

            if(argv[i][1]=='-'){
                //说明用户输入 “--”参数作为选项的结束标志
                break;
            }

            for( k=1;k<strlen(argv[i]);k++){


                if(argv[i][k]=='a'){
                    if(g_user_option&PARAMETER_A){
                        //用户已经输入过a选项了
                        DBGLOG("Warnning: you have already choose option 'a'\n");

                    }else{

                        g_user_option+=PARAMETER_A;

                    }

                    continue;

                }else if(argv[i][k]=='F'){

                    if(g_user_option&PARAMETER_F){
                        //用户已经输入过 F 选项了
                        DBGLOG("Warnning: you have already choose option 'f'\n");

                    }else{

                        g_user_option+=PARAMETER_F;

                    }

                    continue;

                }else if(argv[i][k]=='i'){

                    if(g_user_option&PARAMETER_I){
                        //用户已经输入过 i 选项了
                        DBGLOG("Warnning: you have already choose option 'i'\n");

                    }else{

                        g_user_option+=PARAMETER_I;

                    }

                    continue;

                }else if(argv[i][k]=='d'){

                    if(g_user_option&PARAMETER_D){
                        //用户已经输入过 d 选项了
                        DBGLOG("Warnning: you have already choose option 'd'\n");

                    }else{

                        g_user_option+=PARAMETER_D;

                    }

                    continue;

                }else if(argv[i][k]=='l'){

                    if(g_user_option&PARAMETER_L){
                        //用户已经输入过 l 选项了
                        DBGLOG("Warnning: you have already choose option 'l'\n");

                    }else{

                        g_user_option+=PARAMETER_L;

                    }

                    continue;

                }else if(argv[i][k]=='R'){

                    if(g_user_option&PARAMETER_R){
                        //用户已经输入过 R 选项了
                        DBGLOG("Warnning: you have already choose option 'R'\n");

                    }else{

                        g_user_option+=PARAMETER_R;

                    }

                    continue;

                }else{
                    //输入了错误的字符作为选项
                    DBGLOG("%s: invalid option -'%c'\n",argv[0],argv[i][k]);
                    exit(-1);

                }


            }/*END OF FOR*/


            argc--; //等下方便计算文件名参数个数来做while循环的控制

            file_index++;  //文件名实参的下标+1

        }

    }
    //test
    //printf("At last user_option is %d\n",g_user_option);
    int j=0;
    for(j=0;j<file_index-1;j++){
        *(argv++);
    }

    if(argc==1&&g_user_option!=0){//有选项 无文件名实参

    //test_option();
    do_ls(".");

    }else{



    //test_option();
    while(--argc){
        printf("%s:\n",*++argv);
        chdir(*argv);       //切换工作目录到对应的路径 如果argv并非是目录文件，也可以这么做
        do_ls(*argv);
        chdir("");          //切换工作目录到当前目录 这一步是必要的,因为实参可能为相对路径
    }


    }


    }




    printf("\n");
    return 0;
}


void test_option(){         //用于检测程序对 用户输入的选项 的识别结果

    int local_user_option=g_user_option;    //暂存用户选项值，方便之后处理，不会影响到全局变量g_user_option

    if(local_user_option&PARAMETER_A){

    local_user_option-=PARAMETER_A;

    printf(" 'a' ");

    }
    if(local_user_option&PARAMETER_F){

    local_user_option-=PARAMETER_F;

    printf(" 'F' ");

    }
    if(local_user_option&PARAMETER_I){

    local_user_option-=PARAMETER_I;

    printf(" 'i' ");

    }
    if(local_user_option&PARAMETER_L){

    local_user_option-=PARAMETER_L;

    printf(" 'l' ");

    }
    if(local_user_option&PARAMETER_D){

    local_user_option-=PARAMETER_D;

    printf(" 'd' ");

    }
    if(local_user_option&PARAMETER_R){

    local_user_option-=PARAMETER_R;

    printf(" 'R' ");

    }
    if(g_user_option!=0){
        printf("\n");
    }


}

void do_ls(char* dirname){//访问各个目录



    char buf[80];//存储当前工作目录 路径

    DIR * dir_ptr;
    struct dirent *dirent_ptr;


    struct stat file_stat_buf;

    if( (dir_ptr=opendir(dirname))==NULL ){

            //如果不是目录文件，就会报错，并且直接进行do-stat()处理
            //DBGLOG("Cannot open %s , not a directory, treat as a file shown below:\n",dirname);
            //这里并不退出进程 ，因为在ls命令中实参可以使用文件名
            do_stat(dirname);

    }else{  //是目录文件，需要用readdir()

        if(g_user_option&PARAMETER_D){//制定了-d选项

            if(dirname=="."){
                printf(" .\n");
            }else{

                do_stat(dirname);

            }
            return;

        }else{
                //printf("g_user_option %d %d %d %d \n",g_user_option,g_recursion==0,(g_user_option&PARAMETER_R),((g_user_option&PARAMETER_D)==0 ));

                if(   g_recursion==0&&((g_user_option&PARAMETER_R)!=0 )&&((g_user_option&PARAMETER_D)==0 )  ){

                    //因为存在这样一个问题，dirname如果为“.”，我们需要把他变成绝对路径名，以免之后产生歧义

                    getcwd(buf,sizeof(buf));



                    strcpy(path,buf);       //用于暂存文件实参目录路径名，便于处理完子目录后chdir（）回来




                    //需要处理下一层子目录的内容，而且当前目录还需要打印一个".: \n"
                    g_recursion=1;

                    printf("%s: \n\n",dirname);

                    while( (dirent_ptr=readdir(dir_ptr))!=NULL ){

                    do_stat(dirent_ptr->d_name);

                    if( stat(dirent_ptr->d_name,&file_stat_buf)==-1 ){//读文件信息失败

                        DBGLOG("Get %s file_info error \n",dirent_ptr->d_name);

                        exit(-1);


                    }else{//读文件信息成功

                        if(S_ISDIR(file_stat_buf.st_mode)){
                            //是目录，需要记录此目录的路径信息并进行递归do_ls()

                            strcpy(r_dir_arr[r_dir_number++],dirent_ptr->d_name);

                        }

                    }

                    }

                    //检测子目录是否已经录入
                    int i=0;
                    for(i=0;i<r_dir_number;i++){

                        //printf("第%d个子目录为: %s \n",i,r_dir_arr[i]);

                    }


                }


                while( (dirent_ptr=readdir(dir_ptr))!=NULL ){


                    do_stat(dirent_ptr->d_name);

                }
                //如果需要进行递归处理，下面进行对所有一层子目录的do_ls（）处理

                if(g_recursion==1 ){






                    while(r_dir_index<r_dir_number){


                        if(r_dir_arr[r_dir_index][0]=='.'&& ((g_user_option&PARAMETER_A)==0) ){ //如果是隐藏文件，切记不要进行处理
                            //printf("r_dir_index 指向 子目录 %s  为隐藏子目录，不处理此目录，跳过\n",r_dir_arr[r_dir_index]);
                            r_dir_index++;
                            continue;
                        }else{

                                DIR * child_dir_ptr;
                                struct dirent *child_dirent_ptr;

                               // printf("\n开始处理%s 子目录\n",r_dir_arr[r_dir_index]);
                                //切换工作目录前的准备，制作一个充当chdir()路径参数的字符串

                                char tmp[PATH_MAX+1];

                                int end=0;
                                while(path[end]){
                                    end++;
                                }

                                strcpy(tmp,path);
                                if(path[end-1]!='/'){

                                    strcat(tmp,"/");

                                }
                                strcat(tmp,r_dir_arr[r_dir_index]);


                                getcwd(buf,sizeof(buf));
                                //printf("打开句柄之前:%s pwd: %s\n",tmp,buf);


                                if( (child_dir_ptr=opendir(tmp))==NULL ){

                                        //如果不是目录文件，就会报错，并且直接进行do-stat()处理
                                        DBGLOG("Warnning-Cannot open child_dir %s : %s\n",tmp,strerror(errno));
                                        //exit(-1);  不退出，因为可能打不开的原因时permission denied


                                }else{

                                    chdir(tmp);
                                    //需要先打印一下这个路径的名字
                                    getcwd(buf,sizeof(buf));
                                    printf("\n%s :\n\n",buf);
                                    while( (child_dirent_ptr=readdir(child_dir_ptr))!=NULL ){



                                        do_stat(child_dirent_ptr->d_name);



                                    }
                                    //printf("path=%s\n",path);
                                    chdir(path);
                                    getcwd(buf,sizeof(buf));
                                    //printf("处理完 %s 目录的文件后，切换工作目录回来。；pwd: %s\n",r_dir_arr[r_dir_index],buf);

                                }
                                r_dir_index++;

                        }



                        //遍历完了子目录
                        g_recursion=0;

                    }// end of  while(r_dir_index<r_dir_number)





                }



        }




    }

   if( closedir(dir_ptr)==-1 ){    //关闭目录句柄失败

        DBGLOG("Closedir %s Error: %s\n",dirname,strerror(errno));

        exit(-1);
    }

}

void do_stat(char* filename){//获取各个文件信息

    struct stat file_info;


    if( stat(filename,&file_info)==-1 ){//读文件信息失败

        DBGLOG("Get %s file_info error :%s\n",filename,strerror(errno));

        exit(-1);


    }else{//读文件信息成功


        show_file_info(filename,&file_info);

    }



}

void show_file_info(char * filename,struct stat *stat_buffer){//展示单个文件的信息



    if( (g_user_option&PARAMETER_A)==0&& filename[0]=='.' ){
        //假设未指定-a选项 且本文件为隐藏文件，直接return即可
        //printf("%d %c\n",g_user_option&PARAMETER_A,filename[0]);
        //printf("%d %d\n",(g_user_option&PARAMETER_A)==0,filename[0]=='.');
        return;
    }
    char str[11]="";



    mode_to_str(stat_buffer,str,filename);

    if(g_user_option&PARAMETER_I){
        //用户指定了-i选项
        printf("%-10d ",(int)stat_buffer->st_ino);
    }

    if(g_user_option&PARAMETER_L){
        printf("%s ",str);

        printf("%4d ",(int)stat_buffer->st_nlink);

        printf("%-8s ",uid_to_name(stat_buffer->st_uid));

        printf("%-8s ",gid_to_name(stat_buffer->st_gid));

        printf("%8ld ",(long)stat_buffer->st_size);

        printf("%.12s  ",4+ctime(&stat_buffer->st_mtime));
    }



    printf("%s",filename);
    if(g_user_option&PARAMETER_F){
        printf(" %c",file_type_char(stat_buffer->st_mode,stat_buffer->st_uid));
    }



    printf("\n");


}

void mode_to_str(struct stat *file_info, char * str,char * filename){  //将文件mode转换成一个长度为11的C风格字符串（第10位存的是'\0'）

    strcpy(str,"----------");

    switch (file_info->st_mode & S_IFMT) {
           case S_IFBLK:
               str[0]='b';
               //printf("block device\n");
               break;
           case S_IFCHR:
               str[0]='c';
               //printf("character device\n");
               break;
           case S_IFDIR:
               str[0]='d';
               //printf("directory\n");
               break;
           case S_IFIFO:
               str[0]='p';
               //printf("FIFO/pipe\n");
               break;
           case S_IFLNK:
               str[0]='s';
               //printf("symlink\n");
               break;
           case S_IFREG:
               str[0]='-';
               //printf("regular file\n");
               break;
           case S_IFSOCK:
               str[0]='s';
               //printf("socket\n");
               break;
           default:
               DBGLOG("Warnning:%s 's file_type is unknown.\n",filename);
               //printf("unknown?\n");
               break;
           }



    //文件属主权限
    if(file_info->st_mode & S_IRUSR){
        str[1]='r';
    }else{
        str[1]='-';
    }
    if(file_info->st_mode & S_IWUSR){
        str[2]='w';
    }else{
        str[2]='-';
    }
    if(file_info->st_mode & S_IXUSR){
        str[3]='x';
    }else{
        str[3]='-';
    }
    //组权限
    if(file_info->st_mode & S_IRGRP){
        str[4]='r';
    }else{
        str[4]='-';
    }
    if(file_info->st_mode & S_IWGRP){
        str[5]='w';
    }else{
        str[5]='-';
    }
    if(file_info->st_mode & S_IXGRP){
        str[6]='x';
    }else{
        str[6]='-';
    }
    //其他用户权限
    if(file_info->st_mode & S_IROTH){
        str[7]='r';
    }else{
        str[7]='-';
    }
    if(file_info->st_mode & S_IWOTH){
        str[8]='w';
    }else{
        str[8]='-';
    }
    if(file_info->st_mode & S_IXOTH){
        str[9]='x';
    }else{
        str[9]='-';
    }





}

char * uid_to_name(uid_t uid){
    struct passwd * user;
    static char name[10];
    if(  (user=getpwuid(uid))==NULL ){

    sprintf(name,"%d",uid);

    }else{

    sprintf(name,"%s",user->pw_name);

    }

    return name;

}
char * gid_to_name(gid_t gid){

    struct group * group_ptr;

    static char group_name[10];

    if(  (group_ptr=getgrgid(gid))==NULL ){

    sprintf(group_name,"%d",gid);

    }else{

    sprintf(group_name,"%s",group_ptr->gr_name);

    }

    return group_name;

}
char file_type_char(mode_t    st_mode,uid_t uid){ //得到文件对应的类型字符


    char c;
    uid_t my_euid;		//有效uid
    if((st_mode & S_IFMT)==S_IFDIR){
        c='/';
    }

    if( uid == geteuid() && st_mode & S_IXUSR){
        c='*';
    }	

    return c;


}


//quick start
/*

$ cc myls.c -o myls
$ ./myls [-R -l -d -i -F -a] [file_name_list]

*/
//思考程序的不足
/*
1.时间比较紧，没有充分考虑鲁棒性，比如
./myls -l -l
这样的输入错误没进行对应的处理

2.ls -l
最上边的“总用量” 不知道是怎么计算的，没完善这个功能

3.myls 列出的文件并未排序

4.对于符号链接文件，能否打印出 symbollink -> file 的形式

5.对于-R选项，此程序只进行到文件名实参的下一层的递归
另外，对于当前路径下的子目录xxx，在显示时，也不会显示成./xxx
而是直接显示成绝对路径名，
这一点我觉得可以这么解决
我们可以在程序的main函数体中将当前工作目录绝对路径名存储到一个字符串中，
在-R 选项下的子目录名显示时，进行一下字符串替换，把绝对路径替换成./xxx的形式
emmm..   上面的功能我怕麻烦，所以不做
                    --------- =.=

6.文件显示未排序（可以在文件信息外封装一个链表来排序）

7.另外在测试时也有一些BUG 不明白错误出现的原因，比如
$sudo ./myls -R /usr
最后一行提示我某个文件打不开
do_stat -- 483 -- Get nvidia-uninstall file_info error :No such file or directory

8.-F选项的实现实际上是阉割版
因为在实现 -F 选项时发现一个问题——如何判断文件是否为可执行文件
并不能通过下面这种方式来判断文件是否为可执行文件
if((st_mode & S_IXUSR) &&(st_mode & S_IXGRP)  && (st_mode & S_IXOTH))
网上找到一个函数access(),由于时间比较赶，我就没试这个函数是否能解决问题
暂时没解决如何判断文件是否为可执行文件的问题。

9.对于不指定-l选项的文件，
为了省事不去进行排版，直接每行列出一个文件，
*/